第4章 逆向分析技术

32位

函数

约定类型 __cdecl(C规范) stdcall fastcall pascal
参数传递顺序 从右到左 从右到左 寄存器和栈 从左到右
平衡栈者 调用者 子程序 子程序 子程序
允许使用VARARG

VARARG:表示参数可以不确定;stdcall如果使用VARARG,就是调用程序平衡栈,否则就是被调用程序平衡栈

C/C++和MFC程序默认使用__cdecl

stdcall是Win32 API采用的约定方式,Win32 API中,也有一些函数是__cdecl调用约定,如wsprintf

VC6.0优化选项设置为"Maximize Speed",可能会用esp来对参数寻址(节省对ebp的压栈操作)

fastcall:不同编译器实现不同。Microsoft Visual C++ 左边的2个不大于4字节的参数分别放在ecx和edx,其余从右到左压栈

thiscall:C++中非静态类成员函数的默认调用约定,ecx传递this指针

数据结构

静态变量和全局变量类似,都是有赋值放.data段没赋值放.bss段,但静态变量仅在定义这些变量的函数内有效

虚函数

所有对虚函数的引用都放在一个数组中,这个数组叫虚函数表(VTBL),数组的每个元素中存放的就是类中虚函数的地址。调用虚函数时,程序先取出虚函数表指针(VPTR),再去取该函数地址。

控制语句

开启Maximize Speed时,switch-case中,会用dec指令代替cmp指令

转移指令=转移类别机器码+位移量(目的地址-起始地址-跳转指令长度=目的地址-EIP)

条件设置指令SETcc r/m8(cc为JBE、JE中的BE、E之类的),作用是根据标志位把结果记录到目标操作数,可以用来消除程序中的转移指令

循环语句

一般将ecx寄存器作为计数器

数学运算

除法中,商放入eax,余数放入edx

字符串

优化后的strlen:

mov ecx,FFFFFFFF;看到这一句很可能要获取字符串长度
sub eax,eax
repnz           ;重复串操作直到ecx=0
scasb           ;把AL的内容与edi指向的附加段中的字节逐一比较
not ecx
dc ecx
je xxxxxx

rep stos指令:

rep:重复ecx次

stos:将eax中的值拷贝到es:edi中,其中stosb/stosw/stosd分别对应一次填½/4字节

64位

函数

调用约定:前4个参数用寄存器传递,顺序是RCX、RDX、R8、R9,超过4个栈传参,从右到左入栈,函数调用方平衡栈。虽然前4个参数用寄存器传参,但栈仍然为这4个参数预留空间。

参数为结构体且小于等于8字节,直接把结构体内容放在寄存器;大于8字节时,会先把结构内容复制到栈空间,再把结构体地址当成函数参数来传递

thiscall:类的成员函数调用隐含通过rcx传递this指针

数据结构

局部变量和全局变量都是先定义的在低地址,后定义的在高地址

控制语句

switch-case分支数小于6时直接用if-else

case>=6且case值间隔较小时,会采用case表,表中存的是要跳转的case位置偏移。对应第几项是以case的最小数为基准的,比如case有100、101、102、103、104、105六种情况,那么在表中的偏移就是100-100、101-100、102-100、103-100、104-100、105-100,避免了需要100个case表项填default地址

case项较多时,用二叉平衡树存case值,通过汇编看跟if-else相似

循环语句

循环与if语句最大的区别是循环可以向上跳转

while循环比do循环多一次if判断,release版本会把while优化成等价的do循环

数学运算

除数为2^n,被除数为正数则直接右移n位,负数看书P159(公式太难打了)

除数为非2^n,会使用魔数进行优化,有2种公式,根据魔数正负可以快速辨别公式

虚函数

无继承的虚表

虚函数的作用主要是实现多态,可以用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。

如果一个类有虚函数才会有虚表;虚表不只有虚函数,非虚函数也会记录

构造函数完成会返回this指针

析构函数首先也会赋值虚表,最后也返回this指针,赋值虚表原因是析构函数需要调用虚函数的无多态性

只写一个析构函数会生成两个析构函数,一个是普通析构函数,对象出作用域时调用;一个放在虚表,在delete对象的时候调用。虚表中的析构函数也是调用普通析构函数,但会在最后多一个delete this的操作

单重继承虚表

基类和派生类有各自虚表,但在对象中是共享一个虚表指针

派生类虚表填充过程(由编译器产生):

  1. 复制虚类的基表
  2. 如果派生类虚函数有重载,用派生类的虚函数地址覆盖对应表项
  3. 如果派生类有新增虚函数,在虚表后面补充

多重继承虚表

如果有两个有虚函数的基类,派生类就有两个虚表

派生类新增的虚函数挂在第一个虚表后面

明显的特征是多次初始化虚表

菱形继承虚表

例子:B虚继承A,C虚继承A,D继承B、C(内存布局看书P190,非常清晰)

因为存在多重继承,所以D在构造时会调用B、C的构造函数,B、C又会调用A的构造函数,为避免重复,调用D的构造函数时会传递一个标志(1为调用)来说明是否调用A的构造函数

因为存在虚继承,虚基类对象的内存在派生类的内存中只保留一份,为了能方便定位虚基类在对象内存中的位置,做了一个**虚基类偏移表**,共8字节,高4字节未发现作用,低4字节用于表示虚基类在当前虚基类偏移表中的偏移。

如果是虚继承,就不再与基类共享一个虚表,会增加一个虚表

抽象类虚表

与单重继承区别不大,唯一区别在于虚表

由于抽象类中有纯虚函数,纯虚函数没有实现代码,因此编译器默认填充_purecall函数的地址

_purecall函数的功能就是显示一个错误信息并退出程序